Core Java & Languages

Kotlin vs. Scala: Showdown der funktionalen Programmierung auf der JVM

Wie funktional ist Kotlin? Oder sollten wir doch in Scala programmieren?

Dr. Michael Sperber

Kotlin ist eine Weiterentwicklung von Java mit zusätzlichen Features und Vereinfachungen. Die Ideen dafür kamen überwiegend aus der funktionalen Programmierung. Kein Wunder, bietet diese doch gegenüber OOP viele Vorteile, wie architektonische Entkopplung durch unveränderliche Daten und bessere Domänenmodelle durch mächtigere Abstraktionen. Aber reichen Kotlins Innovationen aus, um es zu einer waschechten funktionalen Programmiersprache zu machen?

Der Begriff „funktionale Programmiersprache“ gründete sich ursprünglich darauf, dass die Sprache Funktionen „erstklassig“, also als normale Objekte behandelt und erlaubt, sie zur Laufzeit zu erzeugen. Bei statisch typisierten Sprachen (und dazu gehört auch Kotlin) bedeutet der Begriff außerdem, dass es das Typsystem erlaubt, Funktionen zu beschreiben, und die erstklassige Manipulation von Funktionen zumindest nicht behindert.

Ein wichtiger pragmatischer Aspekt funktionaler Programmierung (FP) ist die Verwendung unveränderlicher (immutable) Daten. Diese kann eine Programmiersprache mehr oder weniger gut unterstützen. Dazu gehört auch die Möglichkeit, Funktionen ohne die Verwendung von Zuweisungen zu schreiben. Schließlich zählt die Unterstützung von höherstehenden Abstraktionen wie Monaden zur Standardausrüstung funktionaler Sprachen.

Unveränderliche Daten

Das mit den unveränderlichen Daten klingt eigentlich ganz einfach: Wir verzichten auf Zuweisungen, fertig. Damit das praktikabel wird, gehört allerdings doch mehr dazu. Wenn eine Klasse unveränderliche Objekte beschreibt, ist es meist sinnvoll, einen Konstruktor zu definieren, der für jedes Attribut der Klasse ein Argument akzeptiert, außerdem equals– und hashCode()-Methoden, die ausschließlich über die Gleichheit beziehungsweise die Hashfunktion der Attribute definiert sind, Ähnliches gilt für die toString()-Methode. In Java hat die IDE, die Konstruktor und Methoden automatisch generieren kann, diesen Job traditionell erledigt. Danach liegt aber eine Menge Code herum, der nur das Offensichtliche tut und Übersicht sowie Wartung behindert.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Glücklicherweise hat Kotlin sogenannte Data Classes, in denen dieser Code automatisch vom Compiler generiert wird. Das ist so eine Klasse:

data class Rectangle(val length: Double, val width: Double)

Das data vor class sorgt für die automatische Generierung von Konstruktor und Methoden, val sorgt dafür, dass die entsprechenden Attribute nicht verändert werden können. (Weil das so praktisch ist, hat Java längst mit Records nachgezogen.)

Bei funktionalen Sprachen sind Pendants zur Java Collections Library dabei, die allerdings ebenfalls ohne Veränderungen auskommen. Bei Kotlin sind scheinbar unveränderliche Collections dabei. So hängen wir zum Beispiel eine Liste an eine andere Liste:

val list1 = listOf(1,2,3)
val list2 = listOf(4,5,6)
val list3 = list1 + list2

Hier ist list3 eine neue Liste mit den gleichen Elementen wie list1 (die unverändert bleibt) und den Elementen von list2 zusätzlich dahinter. Hinter den Kulissen tut aber die gute alte Java ArrayList ihren Dienst: Das bedeutet, dass die Operation + list2 ein neues Array für list3 anlegt und alle Elemente sowohl von list1 als auch von list2 nach list3 kopiert – und das ist teuer. Aus diesem Grund verwenden die Collection Libraries funktionaler Sprachen sogenannte funktionale Datenstrukturen, die nicht bei jedem Update alles kopieren müssen. Insbesondere ist eine Liste in der funktionalen Programmierung eine konkrete Datenstruktur, die bei allen (anderen) funktionalen Sprachen fest eingebaut ist. Wir können sie aber in Kotlin nachbauen:

sealed interface List<out A>
object Empty : List<Nothing>
data class Cons<A>(val first: A, val rest: List<A>) : List<A>

Eine Liste ist also entweder Empty (die leere Liste) oder eine sogenannte Cons-Liste, bestehend aus erstem Element und rest-Liste (es handelt sich also um eine unveränderliche, einfach verkettete Liste). FP-Pendants zu den ersten beiden Listen von oben können wir so konstruieren:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))
val list2 = Cons(4, Cons(5, Cons(6, Empty)))

Das ist etwas umständlich, verglichen mit listOf, dieses Manko könnten wir aber mit einer einfachen varargs-Funktion beheben. Die plus-Methode auf den eingebauten Kotlin-Listen können wir für die FP-Listen ebenfalls implementieren:

fun <A> List<A>.append(other: List<A>): List<A> =
  when (this) {
    is Empty -> other
    is Cons ->
       Cons(this.first, this.rest.append(other))
  }

Das Beispiel von oben funktioniert mit FP-Listen so:

val list3 = list1.append(list2)

Die append-Methode wirft ein paar Fragen auf: Warum ist sie eine „Extension Method“? Könnte das bei der Rekursion Probleme machen? Dazu später mehr, hier geht es ja erst einmal um unveränderliche Daten: Und tatsächlich verändert append gar nichts, kopiert die Elemente aus list1, hängt aber list2 an, ohne sie zu kopieren. Hinterher teilen sich also list3 und list2 dieselben Cons-Objekte. Weil Listen unveränderlich sind, ist das unproblematisch: Das Programm kann so tun, als wären list2 und list3 völlig unabhängig voneinander. Die append-Methode verbraucht nur so viel Laufzeit und Speicher, wie list1 lang ist – die Länge von list2 ist völlig irrelevant.

Listen sind für viele Situationen geeignet, aber natürlich nicht für alle – wenn list1 lang ist, braucht append auch mehr Zeit und viel Speicherplatz. Entsprechend haben die Collection Libraries funktionaler Sprachen in der Regel auch noch ausgefeiltere Datenstrukturen auf der Basis von Bäumen beziehungsweise Trees im Gepäck, bei denen die meisten Operationen quasi konstant laufen.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Solche funktionalen Datenstrukturen gibt es auch für Java und Kotlin in Form der Vavr-Library [1], die neben FP-Listen auch Vektoren mitbringt. Für Vavr gibt es auch das Add-on vavr-kotlin [2], das die Vavr Collections in Kotlin noch etwas idiomatischer zugänglich macht. Zwischenfazit: Funktionale Datenstrukturen sind bei Kotlin nicht standardmäßig dabei, können aber in Form von Vavr nachgeladen werden. Tolle Sache.

Erstklassige Funktionen

Kommen wir zur Unterstützung für Funktionen als Objekte: Da macht Kotlin gegenüber Java einen riesigen Sprung durch zwei Neuerungen:

  • Das Kotlin-Typsystem kennt Funktionstypen, die man sich in Java umständlich als Single-Method-Interface selbst definieren muss.
  • Kotlin macht effektiv keinen Unterschied zwischen Value Types und Reference Types.

Was das bringt, zeigt die Implementierung des FP-Klassikers map, den eine Funktion (die als Objekt übergeben wird) auf alle Elemente einer Liste anwendet und dann eine Liste der Ergebnisse zurückliefert:

fun <A, B> List<A>.map(f: (A) -> B): List<B> =
  when (this) {
    is Empty -> Empty
    is Cons ->
     Cons(f(this.first),
       this.rest.map(f))
  }

Super-Sache:

val list4 = list3.map { x -> x + 1 }

Das geht natürlich auch in Java, das Pendant sieht fast identisch aus:

var list4 = list3.map(x -> x + 1);

Allerdings funktioniert das in Java nur so richtig gut, wenn der Lambdaausdruck unmittelbar im Methodenaufruf steht. In dem Moment, in dem wir ihn herausziehen, sieht die Sache anders aus. In Kotlin ist das kein Problem:

val inc = { x: Int -> x + 1 }
val list4 = list3.map(inc)

Wir müssen bei inc den Typ von x angeben, weil noch andere Typen die Operation + 1 (Double, Long) unterstützen. Den Typ der Funktion inferiert Kotlin nun als (Int) -> Int. In Java müssen wir uns allerdings beim Typ von inc für ein Interface entscheiden – am Naheliegendsten ist IntUnaryOperator:

IntUnaryOperator inc = x -> x + 1;

Allerdings können wir den nicht an map übergeben – es kommt zu einer Fehlermeldung: „‘map(Function<Integer,B>)’ cannot be applied to ‘(IntUnaryOperator)’“. Das liegt daran, dass map ein Argument des Typs Function<A, B> akzeptiert – wir mussten uns ja für ein spezifisches Interface entscheiden. Leider passen Function und IntUnaryOperator nicht zueinander, weil Function auf Referenztypen besteht, während IntUnaryOperator auf den Value Type int festgelegt ist. Die Konsequenz ist, dass zum Beispiel das Stream-Interface in Java neben map auch mapToInt, mapToDouble und mapToLong anbieten muss, die jeweils ein IntStream, DoubleStream und LongStream produzieren. Das ist umständlich und macht fortgeschrittene Anwendungen von Funktionen höherer Ordnung in Java unpraktikabel – in Kotlin kein Problem, ein weiterer Pluspunkt.

Schleifen und Endrekursion

Zu erstklassiger Unterstützung von Funktionen gehört auch, dass wir in der Lage sein sollten, Funktionen ohne Verwendung von Zuweisungen zu schreiben. Dafür sind append und map gute Beispiele – rein funktional programmiert. Sie haben aber beide denselben Haken: Für lange Listen funktionieren sie nicht, weil sie einen Stack-Overflow verursachen. Das liegt an einigen Eigenheiten der JVM: Sie muss sich merken, wie es nach dem rekursiven Aufruf weitergeht (mit Cons) und verwendet dafür einen speziellen Speicherbereich, den Stack. Dieser Stack ist in seiner Größe beschränkt und viel kleiner als der Hauptspeicher, der eigentlich zur Verfügung stünde. Außerdem ist die Größe beim Start der JVM festgelegt und später nicht mehr zu ändern. Funktionale Sprachen mit eigener Runtime haben dieses Problem meist nicht, weil sie flexiblere Datenstrukturen anstatt eines Stacks mit fester Größe verwenden.

fun <A> List<A>.append(other: List<A>): List<A> {
  var list = this.reverse()
  var acc = other
  while (list is Cons) {
    acc = Cons(list.first, acc)
    list = list.rest
  }
  return acc
}

In Java wäre der einzige Weg, das Problem zu vermeiden, eine Schleife – und Schleifen benötigen Zuweisungen, um von einem Durchlauf zum nächsten zu kommunizieren. In Kotlin sieht das so aus wie in Listing 1. Die neue append-Funktion benutzt zwei Schleifenvariablen: die Eingabeliste und das Zwischenergebnis, den Akkumulator. Die Eingabeliste wird am Anfang umgedreht, weil Listen von hinten nach vorn konstruiert werden.

Das neue append funktioniert, ist aber aus funktionaler Sicht schlecht – insbesondere, weil while-Schleifen fehleranfällig sind. Werden zum Beispiel die beiden Zuweisungen an acc und list vertauscht, funktioniert die Methode nicht mehr. Diese Methode können wir auch ohne Zuweisungen mit einer Hilfsfunktion schreiben, die den Akkumulator als Parameter akzeptiert (Listing 2).

fun <A> List<A>.append(other: List<A>): List<A> =
  appendHelper(this.reverse(), other)

fun <A> appendHelper(list: List<A>, acc: List<A>): List<A> =
  when (list) {
    is Empty -> acc
    is Cons ->
      appendHelper(list.rest, Cons(list.first, acc))
  }

Diese Version kommt ohne Zuweisungen aus und benutzt einen rekursiven Aufruf statt der while-Schleife. (Die Hilfsfunktion könnten wir auch lokal innerhalb von append definieren – ein weiterer Punkt für FP-Support in Kotlin.) Anders als der rekursive Aufruf der ursprünglichen append-Version ist dieser hier ein sogenannter Tail Call – also ein Aufruf, der am Ende der Funktion steht.

 

Solche Tail Calls brauchen eigentlich keinen Platz auf dem Stack – entsprechend bestünde bei appendHelper auch keine Gefahr, dass der Stack überläuft. „Eigentlich“ deshalb, weil die JVM leider auch für Tail Calls völlig unnötig Stackplatz verbraucht. (Das ist ein seit mindestens 1996 bekannter Bug, und FP-Fans fiebern mit jedem neuen JVM-Release darauf, dass endlich ein Fix dabei ist.) Wir können aber den Kotlin-Compiler überreden, den rekursiven Tail Call intern in eine Schleife zu kompilieren, indem wir vor die Funktionsdefinition das Wort tailrec schreiben. Das geht leider nur bei statischen Selbstaufrufen (insbesondere also nicht bei Aufrufen von Methoden, die spät gebunden werden), aber immerhin, mehr ist auf der JVM auch nicht drin. Zwischenfazit: Funktionen sind in Kotlin tatsächlich so erstklassig, wie es auf der JVM geht. Die Richtung stimmt also auch hier.

Typen

Kommen wir nochmal zu den Typen: Wir haben ja schon gesehen, dass Kotlin mit Funktionstypen eine wesentliche Anforderung funktionaler Programmierung erfüllt. Leider hat das Typsystem aber auch einige Tücken. Um die zu verstehen, werfen wir einen Blick darauf, wie die Definition von Listen in der funktionalen Programmiersprache Haskell aussehen würde (wenn Listen da nicht schon eingebaut wären):

data List a = Empty | Cons a (List a)

Die Struktur ist ähnlich wie die Kotlin-Definition, aber zwei Unterschiede fallen auf: In Kotlin steht beim Typparameter A noch ein out davor. Außerdem steht beim Empty-Objekt Nothing als Typparameter von List.

Das kommt daher, dass Kotlin versucht, sowohl OO als auch FP zu unterstützen: OO braucht Klassenhierarchien, was auf der Typebene zu Subtyping führt. Deshalb ist es dort notwendig, dass bei Empty ein Typparameter für List angegeben wird – und da der Typparameter A für den Typ der Elemente der Liste steht, die leere Liste aber keine Elemente hat, müssen wir einen Typ angeben, der zu allen anderen passt. In Kotlin ist das eben Nothing, ein „magischer“ Typ ohne Elemente, der als Subtyp aller anderen Typen fungiert (das Gegenteil von Any sozusagen.) Außerdem ist out notwendig, damit eine Liste aus Elementen bestehen kann, die unterschiedliche Typen haben – solange sie einen Supertyp gemeinsam haben. Falls das kompliziert klingt: ist es. Wir können uns der Sache annähern, indem wir out entfernen. Sofort kommt eine Fehlermeldung, zum Beispiel bei:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))

Abgekürzt lautet diese so: „Type mismatch: inferred type is Int but Nothing was expected“. Das liegt daran, dass Empty den Typ List<Empty> hat, wir aber für die Gesamtliste den Typ List<Int> brauchen. Das out sorgt jetzt dafür, dass List<Empty> ein Subtyp von List<Int> ist und damit alles wieder passt – ein Fall von sogenannter Kovarianz. (Wenn wir in geschrieben hätten, wäre Kontravarianz angesagt und es würde andere Fehlermeldungen geben.)

Das Wort out steht dafür, dass die Methoden von List andere Listen nur als „Output“ produzieren dürfen. Das bedeutet insbesondere, dass wir append nicht als „normale“ Methode innerhalb von List implementieren können, weil sie die zweite Liste als Eingabe akzeptiert. Diese Einschränkung gilt nicht für Extension Methods, weshalb wir append als eine solche definiert haben.

Die Situation ist aber immerhin besser als in Java, das erfordert, die Varianzannotationen an die Methoden zu schreiben. Die Java-Stream-Version von append, genannt concat (ebenfalls als statische Methode realisiert), hat zum Beispiel diese Signatur:

static <T> Stream<T> concat(Stream<? extends T> a,
                            Stream<? extends T> b)

Die Kombination von OO und FP führt also zu einigen Komplikationen, auch wenn Kotlin die Situation besser meistert als Java.

Higher-kinded Types

Kotlin hat sich sichtlich von Scala inspirieren lassen, was die FP-Unterstützung betrifft, aber auch auf einige Aspekte von Scala verzichtet – in der Hoffnung, die aus Scala bekannte Komplexität einzudämmen. Dem sind leider auch nützliche Features des Scala-Typsystems zum Opfer gefallen.

Eine wesentliche Einschränkung gegenüber Scala betrifft sogenannte Higher-kinded Types, die es in Scala gibt, in Kotlin leider nicht. Zur Erläuterung dieser Typen schauen wir uns eine Kotlin-Version des Optional-Typs aus Java an. Er ist nützlich, wenn eine Funktion manchmal ein Ergebnis liefert, manchmal aber auch nicht. Entsprechend gibt es zwei Fälle Some und None, ähnlich wie bei Listen:

sealed interface Option<out A>
object None : Option<Nothing>
data class Some<A>(val value: A) : Option<A>

Wir können für Option eine praktische map-Methode definieren:

fun <A, B> Option<A>.map(f: (A) -> B): Option<B> =
  when (this) {
    is None -> None
    is Some -> Some(f(this.value))
  }

Diese Verwandtschaft mit der map-Methode von List können wir sehen, wenn wir beide Signaturen untereinanderschreiben:

fun <A, B> List  <A>.map(f: (A) -> B): List  <B>
fun <A, B> Option<A>.map(f: (A) -> B): Option<B>

Typen wie List und Option, die eine map-Operation unterstützen, heißen in der funktionalen Programmierung Funktor und spielen dort eine wichtige Rolle. Deshalb liegt es nahe, ein Interface Functor zu definieren, in dem die map-Methode steht. Das könnte so aussehen:

interface Functor<F> {
  fun <A, B> map(f: (A) -> B): F<B>
}

Mit dieser Definition wäre Functor ein Higher-kinded Type: Ein Typkonstruktor (also ein Typ mit Generics), der einen anderen Typkonstruktor als Parameter akzeptiert. Das ist dann aber leider doch ein Schritt zu viel für Kotlin, der Compiler quittiert: „Type arguments are not allowed for type parameters“. Es gibt zwar einen Trick, um Higher-kinded Types zu simulieren, nachzulesen zum Beispiel in dem wunderbaren Buch „Functional Programming in Kotlin“ [3]. Er ist aber so umständlich, dass er in der Praxis kaum anwendbar ist.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Beim Typsystem ist in Kotlin also noch Luft nach oben. Dass es geht, zeigt Scala, das Higher-kinded Types unterstützt und für das es entsprechend Libraries mit höherstehenden Konzepten wie Functor gibt, die in Kotlin einfach nicht praktikabel sind. Fairerweise muss man aber zugegeben, dass Higher-kinded Types auch von manchen „richtigen“ funktionalen Sprachen wie F# nicht unterstützt werden.

Monaden

Wo funktionale Programmierung ist, sind Monaden oft nicht weit: Sie sind dort fürs Grobe zuständig und erlauben die Verknüpfung von Funktionen in sequenziellen Abläufen mit Effekten. Effekte sind Dinge, die neben der reinen Berechnung stattfinden – typischerweise Interaktionen mit der Außenwelt. Effekte können zum Beispiel benutzt werden, um in hexagonaler Architektur zwischen Ports und Adaptern zu vermitteln. Für eine komplette Einführung in Monaden fehlt uns hier der Platz, darum skizzieren wir nur, wie sie in Kotlin funktionieren.

Die Idee ist ganz einfach: Ein sequenzieller Prozess wird durch ein Objekt repräsentiert. Für die Objekte erstellen wir einen speziellen Typ (das ist die Monade), in dem genau die Operationen enthalten sind, die in dem Prozess zulässig sind. Zum Beispiel enthält die kleine Demo-Library kotlin-free-monads [4] ein Interface TtyM, um Prozesse abzubilden, die eine textuelle Ausgabe generiereren:

sealed interface TtyM<out A> {
  fun <B> bind(next: (A) -> TtyM<B>): TtyM<B>
}

Die bind-Operation (manchmal auch flatMap genannt) macht zusammen mit einer sogenannten pure-Operation TtyM zu einer Monade. Der Typparameter A ist für das Ergebnis des Prozesses zuständig. Wenn wir die TtyM-Monade benutzen, verwenden wir also nicht mehr println, sondern konstruieren stattdessen ein Write-Objekt, dessen Klasse TtyM implementiert.

Diese Vorgehensweise hat den Vorteil, dass wir das TtyM-Objekt in unterschiedlichen Kontexten ausführen können. Im normalen Betrieb macht das folgende Funktion, die für jedes Write einfach println ausführt:

fun <A> run(tty: TtyM<A>): A

Für automatisierte Tests wollen wir aber wissen, welcher Text ausgegeben wurde, und dafür gibt es eine Funktion, die die Zeilen des Outputs in eine Liste schreibt:

fun <A> output(tty: TtyM<A>, output: MutableList<String>): A

Das ist praktisch, wenn wir schon ein TtyM-Objekt haben. Aber dessen Konstruktion mit Konstruktor- und Methodenaufrufen ist normalerweise ziemlich umständlich, mit vielen geschachtelten Lambdaausdrücken und Klammern. Aus diesem Grund haben die meisten funktionalen Sprachen eine spezielle, deutlich kompaktere Syntax für monadische Programme. Kotlin bringt eine solche Syntax zwar nicht direkt mit, wir können sie aber mit Hilfe von suspend-Funktionen selbst bauen, sodass ein TtyM-Programm zum Beispiel so gebaut werden kann:

TtyDsl.effect {
  write("Mike")
  write("Sperber")
  pure(42)
}

Das Lambdaargument von effect ist eine suspend-Funktion. Das suspend sorgt dafür, dass der Compiler die Funktion in eine Coroutine transformiert. Die wiederum erlaubt der effect-Funktion, eine monadische Form der Funktion zu extrahieren. Wer sich für die Details interessiert, sei an das Kotlin-free-monads-Projekt [4] verwiesen.

Auch hier also ein Pluspunkt für Kotlin, zumindest gegenüber Java. Die Verwendung von suspend-Funktionen für monadische Syntax ist aber gegenüber funktionalen Sprachen deutlich aufwendiger und auch mit einigen Einschränkungen verbunden, sodass immer noch Luft nach oben bleibt.

Fazit

Mit zwei Fragen sind wir in diesen Artikel gestartet:

  1. Ist Kotlin funktional genug, dass es sich in lohnt, darin funktional zu programmieren?
  2. Ist Kotlin die richtige Sprache, wenn man auf der JVM funktional programmieren möchte?

Die Antwort auf die erste Frage ist ein entschiedenes Ja: Kotlin unterstützt die einfache Definition unveränderlicher Daten, kennt Typen für Funktionen, eliminiert die lästige Unterscheidung zwischen Referenztypen und Value Types, kompiliert rekursive Tail Calls korrekt, vereinfacht gegenüber Java den Umgang mit Generics und erlaubt die Definition monadischer Syntax. Das ist mehr als genug, um funktionale Programmierung gewinnbringend zu nutzen, mächtige Abstraktionen zu definieren und architektonisch von der Entkopplung durch Unveränderlichkeit zu profitieren. Wer mehr darüber wissen möchte, dem sei das oben erwähnte Buch [3] ans Herz gelegt. Eine grundsätzliche Einführung in funktionale Programmierung gibt es in meinem eigenen Buch „Schreibe Dein Programm!“ [5].

Die zweite Frage ist schwieriger zu beantworten: Auf der JVM gibt es zwei (weitere) funktionale Sprachen, nämlich Clojure und Scala. Beide sind großartig – während aber Clojure in vielerlei Hinsicht fundamental anders tickt als Kotlin und Java (kein Typsystem, Lisp-Syntax), ist Scala die Verlängerung der Linie von Java zu Kotlin.

 

Scala unterstützt funktionale Programmierung besser als Kotlin: Es gibt Higher-kinded Types, die Monaden funktionieren in Scala deutlich einfacher und außerdem erleichtern sogenannte Implicits es, automatisch die richtigen Implementierungen für Interfaces zu finden. Des Weiteren existiert um Scala herum ein umfangreiches Ökosystem funktionaler Libraries und Frameworks.

Die gegenüber Scala verringerte Komplexität von Kotlin fällt in der Praxis kaum ins Gewicht. Viele der typischen Schwierigkeiten in der täglichen Arbeit gibt es in Kotlin auch. Die IDE-Unterstützung ist für beide ähnlich gut. Wer also funktionale Programmierung mag und die Wahl hat, wird wahrscheinlich mit Scala glücklicher.


Links & Literatur

[1] Vavr, funktionale Library für Java: https://github.com/vavr-io/vavr

[2] Kotlin-Wrapper für Vavr: https://github.com/vavr-io/vavr-kotlin

[3] Vermeulen, Marco; Bjarnason, Rúnar; Chiusano, Paul: „Functional Programming in Kotlin“; Manning, 2021

[4] Monaden-Library für Kotlin: https://github.com/active-group/kotlin-free-monad

[5] Sperber, Michael; Klaeren, Herbert: „Schreibe Dein Programm!“; Tübingen University Press, 2023

Top Articles About Core Java & Languages

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management

ALLE NEWS ZUR JAX!